/*
 *    Copyright © OpenAtom Foundation.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.inspur.edp.bef.core.session;

import com.inspur.edp.bef.api.be.IBEManager;
import com.inspur.edp.bef.core.DebugAdapter;
import com.inspur.edp.bef.core.action.base.ActionStack;
import com.inspur.edp.bef.core.lock.LockAdapter;
import com.inspur.edp.bef.core.lock.LockService;
import com.inspur.edp.bef.core.session.distributed.dac.FuncSessionDac;
import com.inspur.edp.bef.core.session.distributed.dac.FuncSessionDacFactory;
import com.inspur.edp.cef.api.RefObject;
import com.inspur.edp.cef.entity.changeset.Tuple;
import com.inspur.edp.cef.entity.dependenceTemp.DataValidator;
import com.inspur.edp.commonmodel.core.session.distributed.RootEditTokenImpl;
import com.inspur.edp.commonmodel.core.session.distributed.SessionEditToken;
import com.inspur.edp.commonmodel.core.session.distributed.dac.FuncSessionIncrement;
import com.inspur.edp.commonmodel.core.session.serviceinterface.SessionConfigService;
import com.inspur.edp.commonmodel.core.session.tableentity.SessionConfig;
import io.iec.edp.caf.boot.context.CAFBizContextHolder;
import io.iec.edp.caf.boot.context.CAFContext;
import io.iec.edp.caf.commons.utils.SpringBeanUtils;
import io.iec.edp.caf.core.context.BizContext;
import io.iec.edp.caf.core.context.BizContextBuilderNotFoundException;
import io.iec.edp.caf.core.context.BizContextDestroyFailedException;
import io.iec.edp.caf.core.context.BizContextManager;
import io.iec.edp.caf.core.context.BizContextNotFoundException;
import io.iec.edp.caf.core.context.FrameworkContext;
import io.iec.edp.caf.core.context.ICAFContextService;
import io.iec.edp.caf.runtime.config.CefBeanUtil;
import io.iec.edp.caf.runtime.sessiongroup.api.manager.RuntimeCommonVariableService;
import io.iec.edp.caf.runtime.sessiongroup.api.manager.SessionItemMapService;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.stream.Collectors;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

@Slf4j
public class FuncSessionManager {

  private FuncSessionDac getDefaultDac() {
    return FuncSessionDacFactory.getDac();
  }

  private final RuntimeCommonVariableService tokenProvider;
  private BizContextManager bizContextManager;
  private static ConcurrentHashMap<String, List<String>> tokenBefBizContext;

  // region Constructor
  static {
    privateCurrent = new FuncSessionManager();
  }

  private FuncSessionManager() {
    buckets = new ConcurrentHashMap<>();
    bizContextManager = SpringBeanUtils.getBean(BizContextManager.class);
    tokenProvider = SpringBeanUtils.getBean(RuntimeCommonVariableService.class);
    tokenBefBizContext = new ConcurrentHashMap<>();
//    dac = FuncSessionDacFactory.getDac();
    try {
      FuncSessionUtil.startScheduler(SessionValidityCheck.class, "SessionCheck", 10,1);
    }catch(Throwable t) {
      log.error("SessionValidityCheck定时任务启动失败", t);throw t;
    }
  }
  // endregion

  // region 属性字段
  private ConcurrentHashMap<String, ConcurrentHashMap<String, FuncSession>> buckets;

  ConcurrentHashMap<String, ConcurrentHashMap<String, FuncSession>> getBuckets() {
    return buckets;
  }

  /**当前功能id*/
  public static String getCurrentFuncSessionId() {
    String bc = getCurrBizContextId();
    if(StringUtils.isEmpty(bc)) {
      return null;
    }
    if(getCurrent().getBucket(FuncSessionUtil.getTenantId()).get(bc) != null){
      return bc;
    } else {
      return getCurrent().getSession(bc) != null ? bc : null;
    }
  }

  public static FuncSession getCurrentSession() {
    String bc = getCurrBizContextId();
    return bc == null ? null : getCurrent().getSession(bc);
  }

  /**获取当前功能Session管理器*/
  private static FuncSessionManager privateCurrent;

  public static FuncSessionManager getCurrent() {
    return privateCurrent;
  }
  // endregion

  /**
   * 获取当前功能Session
   *
   * @param funcSessionId 功能SessionID
   * @return 如果传入的SessioinId对应的功能Session已经被释放，返回null
   * @throws NullPointerException 如果传入的funcSessionId为null或Empty,抛出此异常
   */
  public final FuncSession getSession(String funcSessionId) {
    Objects.requireNonNull(funcSessionId);
    FuncSession existing = getBucket(FuncSessionUtil.getTenantId()).get(funcSessionId);
    FuncSession rez = existing != null && !existing.tokens.isEmpty()
        ? existing
        : getLatestSession(funcSessionId, existing);
    if(rez != null) {
      rez.updateLast();
    }
    return rez;
  }

  private FuncSession getLatestSession(String funcSessionId, FuncSession existing) {
    if (existing == null || !getDefaultDac().isLatest(existing.getSessionId(), existing.getVersion())) {
      FuncSession recovered = getDefaultDac().recover(funcSessionId, existing);
      if(recovered != null && existing != recovered) {
        existing = recovered;
        getBucket(FuncSessionUtil.getTenantId()).put(funcSessionId, existing);
      }
    }
    return existing;
  }

  public static ConcurrentHashMap<String, List<String>> getTokenBefBizContextId() {
    return tokenBefBizContext;
  }

  public static void removeBizContextId(String token) {
    tokenBefBizContext.remove(token);
  }

  //创建功能Session
  public final FuncSession initSession() {
    return this.initSession(null);
  }
  public final FuncSession initSession(Duration duration) {
    return initSession(duration, false);
  }

  public final FuncSession initSession(Duration duration, boolean distributable) {
    String curToken = getFrameworkToken();
    String preBefContext = getCurrentFuncSessionId();
    SessionConfig distributedConfig = SpringBeanUtils.getBean(SessionConfigService.class).getSessionConfig();
    distributable = distributable && distributedConfig != null
        && distributedConfig.getConnectInfo() != null && distributedConfig.getConnectInfo().isUseCache();
    BefBizContext bizContext;
    try {
      bizContext = bizContextManager.createRootContext(BefBizContext.class, 0, TimeUnit.SECONDS, BefBizContextSerializer.class.getTypeName());
    } catch (BizContextBuilderNotFoundException e) {
      throw new RuntimeException(e);
    }

    if (!StringUtils.isEmpty(curToken)) {
      Map<String, Object> siMap = SpringBeanUtils.getBean(SessionItemMapService.class)
          .getSessionItemByToken(curToken);
      if (siMap != null)
        bizContext.put(siMap);
    }

    CAFBizContextHolder.setCurrentContext(bizContext);
    DebugAdapter.trace("initSession", DebugAdapter.Mark_Begin, null, bizContext.getId(), distributable);
//      BizContext currContext = CAFContext.current.getBizContext();
    FuncSession funcSession = new FuncSession(duration, distributable);
    funcSession.setSessionId(bizContext.getId());
    funcSession.setUserSessionId(getUserSessionId());
//    getDac().addSession(funcSession.getUserSessionId(), funcSession.getSessionId());
    getBucket(FuncSessionUtil.getTenantId()).put(funcSession.getSessionId(), funcSession);
    if (StringUtils.isEmpty(preBefContext) == false || ActionStack.isEmpty() == false) {
      ActionStack.createNewStackNode(funcSession.getSessionId());
    }
    if(distributable) {
      getDefaultDac().update(funcSession, null);
    }
//      // tag: session性能测试
//      funcSession.setFuncId(frmContext.getFuncId());
    return funcSession;
  }

  //前端专用
  public String initSessionWithToken(String funcInstanceId) {
    FuncSessionDac dac = getDefaultDac();
    Lock funcLock = dac.getFuncInstanceLock(funcInstanceId);
    funcLock.lock();
    try {
      String token = getFrameworkToken();
      FuncSession session = initSession(null, true);
      if (!StringUtils.isEmpty(token)) {
        dac.addSessionWithToken(session.getUserSessionId(), token, session.getSessionId());
      }
      return session.getSessionId();
    } finally {
      funcLock.unlock();
    }
  }

  public void closeSessionWithToken(String sessionId) {
    String token = getFrameworkToken();
    FuncSession session = getSession(sessionId);
    closeSession(sessionId);
    if (!StringUtils.isEmpty(token)) {
      getDefaultDac().removeSessionWithToken(session.getUserSessionId(), token, session.getSessionId());
    }
  }

  @Deprecated
  public String tryInitSessionWithToken() {
    return tryInitSessionWithToken(null);
  }

  public String tryInitSessionWithToken(String funcInstanceId) {
    FuncSessionDac dac = getDefaultDac();
    Lock funcLock = dac.getFuncInstanceLock(funcInstanceId);
    funcLock.lock();
    try {
      String token = getFrameworkToken();
      if (StringUtils.isEmpty(token)) {
        return initSession(null, true).getSessionId();
      }
      String existing = dac.getSessionListByToken(token);
      if (!StringUtils.isEmpty(existing)) {
        try {
          BefBizContext bizContext = bizContextManager.fetch(BefBizContext.class, existing);
          CAFBizContextHolder.setCurrentContext(bizContext);
          return bizContext.getId();
        } catch (BizContextNotFoundException e) {
        }
      }
      FuncSession session = initSession(null, true);
      dac.addSessionWithToken(session.getUserSessionId(), token, session.getSessionId());
      return session.getSessionId();
    } finally {
      funcLock.unlock();
    }
  }

  /**
   * 正常关闭功能Session
   *
   * @param funcSessionId 功能SessionID
   */
  public final void closeSession(String funcSessionId) {
    Objects.requireNonNull(funcSessionId, "funcSessionId");
    DebugAdapter.trace("closeSession", DebugAdapter.Mark_Begin, null, funcSessionId);
    BizContext bizContext = CAFBizContextHolder.getCurrentContext().orElse(null);
    if (bizContext != null) {
      if(funcSessionId.equals(bizContext.getId())) {
        CAFBizContextHolder.releaseCurrentContext();
      }
      SpringBeanUtils.getBean(BizContextManager.class).destroy(funcSessionId);
    }
    FuncSession session = getSession(funcSessionId);
    ActionStack.popPreStackNode();
    if (session == null) {
      DebugAdapter.trace("closeSession", "[Error]funcEnd", null, "session不存在");
      throw new RuntimeException("当前不存在指定ID的Session,关闭Session操作失败");
    }
    clearLock(funcSessionId);
    getDefaultDac().removeSession(funcSessionId);
    removeSessionBucket(funcSessionId);
    DebugAdapter.trace("closeSession", DebugAdapter.Mark_End, null, null);
  }

  /**
   * 非正常关闭功能Session
   *
   * @param funcSessionId 功能SessionID
   */
  public final FuncSession closeSessionWithDestroy(String funcSessionId) {
    DataValidator.checkForEmptyString(funcSessionId, "funcSessionID");
    FuncSession session = getBucket(FuncSessionUtil.getTenantId()).get(funcSessionId);
    if (session != null) {
      session.getLockService().releaseAllLocks();
    }
    try {
      SpringBeanUtils.getBean(BizContextManager.class).destroy(funcSessionId);
    }catch(BizContextNotFoundException e) {}
    LockAdapter.getInstance().removeBatchLockByContext(funcSessionId);
    removeSessionBucket(funcSessionId);
    return session;

  }

  private ConcurrentHashMap<String, FuncSession> getBucket(String tenantId) {
    ConcurrentHashMap<String, FuncSession> rez = buckets.get(tenantId);
    if (rez == null) {
      rez = new ConcurrentHashMap<>(1024, 1024);
      ConcurrentHashMap<String, FuncSession> existing = buckets.putIfAbsent(tenantId, rez);
      if (existing != null) {
        rez = existing;
      }
    }
    return rez;
  }

  @Deprecated
  public final java.util.List<IBEManager> getAllBEManagers() {
    return getCurrentSession().getAllBEManagers();
  }

  public final void clearLock(String funcSessionId) {
    FuncSession session = getSession(funcSessionId);
    LockService service = session.getLockService();
    service.releaseAllLocks();
  }

  /**
   * 移除指定id的Session
   *
   * @param funcSessionId sessionID
   * @return 被移除的Session
   */
  private void removeSessionBucket(String funcSessionId) {
    getBucket(FuncSessionUtil.getTenantId()).remove(funcSessionId);
  }

  //region editable
  private static final long SessionLockWait = 300l;
  @SneakyThrows
  public SessionEditToken beginEdit() {
    String bc = getCurrBizContextId();
    if(bc == null) {
      throwSessionNotFound(bc);
    }
    FuncSession session = getBucket(FuncSessionUtil.getTenantId()).get(bc);
    if(session != null) {
      return beginEdit(session);
    }
    
    Lock lock = getDefaultDac().getSessionLock(bc);
    if(!lock.tryLock(SessionLockWait, TimeUnit.SECONDS)){
      throw new SessionTimeOutException();
    }
    try {
      session = getLatestSession(bc, null);
      if(session == null) {
        throwSessionNotFound(bc);
      }
      SessionEditToken token = session.beginEdit();
      if(token instanceof RootEditTokenImpl) {
        ((RootEditTokenImpl)token).setEditLock(lock);
      }
      return token;
    } catch(Exception e) {
      lock.unlock();
      throw e;
    }
  }

  //TODO: aif手工生单保存触发回写, 回写时手工生单动作已经加锁, 此处再次加锁导致死锁
  public SessionEditToken beginEdit(FuncSession session) {
    return beginEdit(session, true);
  }

  @SneakyThrows
  public SessionEditToken beginEdit(FuncSession session, boolean addLock) {
    Objects.requireNonNull(session, "session");
    Lock lock = null;
    if(addLock && !session.getPassAddLock()) {
      lock = session.isDistributed() ? getDefaultDac().getSessionLock(session.getSessionId())
          : getDefaultDac().getLocalSessionLock(session.getSessionId());

      if (!lock.tryLock(SessionLockWait, TimeUnit.SECONDS)) {
        DebugAdapter.trace("sessionEditTimeout", "error", null,
            session.getSessionId(), session.isDistributed(), lock);
        throw new SessionTimeOutException();
      }
      DebugAdapter.trace("sessionEditSuccess", DebugAdapter.Mark_Running, null,
          session.getSessionId(), session.isDistributed(), lock);
    }
    try {
      if(session.tokens.isEmpty() && session.isDistributed()) {
        session = getLatestSession(session.getSessionId(), session);
      }
      SessionEditToken token = session.beginEdit();
      if (lock != null && token instanceof RootEditTokenImpl) {
        ((RootEditTokenImpl) token).setEditLock(lock);
      }
      return token;
    } catch (Throwable e) {
      if(lock != null) {
        lock.unlock();
      }
      throw e;
    }
  }

  public void endEdit(SessionEditToken token) {
    FuncSession session = getBucket(FuncSessionUtil.getTenantId()).get(token.getSessionId());
    try {
      if(session == null) {
        return;
      }
      session.endEdit(token);
      if (session.isDistributed() && session.tokens.isEmpty()) {
        increment(session, (RootEditTokenImpl) token);
      }
    }catch(Exception e) {
      session.invalid(e);
//      CorruptedSessionException ce = new CorruptedSessionException(e);
      throw e;
    } finally {
      if (token instanceof RootEditTokenImpl && ((RootEditTokenImpl) token).getEditLock() != null) {
        ((RootEditTokenImpl) token).getEditLock().unlock();
      }
    }
  }

  private void increment(FuncSession session, RootEditTokenImpl token) {
    FuncSessionIncrement increment = new FuncSessionIncrement();
    if(token.getIncrements() != null && !token.getIncrements().isEmpty()){
      increment.setItem(new ArrayList<>(token.getIncrements()));
    }
    List<Tuple<String, String>> newSessionItems = session.getSessionItems().values().stream()
        .filter(item -> !token.getItems().contains(item))
        .map(item -> new Tuple<>(item.getCategory(), item.getConfigId()))
        .collect(Collectors.toList());
    if(!newSessionItems.isEmpty()) {
      increment.setItemTypes(newSessionItems);
    }
    increment.setReset(token.getSaved());
    if(session.befContext != null) {
      HashMap<String, Object> orgContextParam = (HashMap<String, Object>) token.getCallContext()
          .get("befcontextparam");
      if(!FuncSessionUtil.compareMap(orgContextParam, session.befContext.getParams())) {
        increment.getCustoms().put("befcontextparam",
            FuncSessionUtil.convertMap2Binary(session.befContext.getParams()));
      }
    }
    if(!increment.isEmpty()) {
      session.setVersion(session.getVersion()+1);
      getDefaultDac().update(session, increment);
    }
  }
  //endregion

  //region util method
  private static final String getCurrBizContextId() {
    BizContext ctx = CAFContext.current.getBizContext();
    return ctx != null ? ctx.getId() : null;
  }

  private final String getFrameworkToken() {
    FrameworkContext frmContext = tokenProvider.getCommonVariableContext();
    return frmContext == null ? null : frmContext.getToken();
  }

  private final String getUserSessionId() {
    String currentSession = CAFContext.current.getSessionId();

    if(StringUtils.isEmpty(currentSession))
      throw new RuntimeException("用户上下文id为空");

    return currentSession;
  }

  private static void throwSessionNotFound(String id) {
    throw new RuntimeException("session不存在:" + id);
  }
  //endregion
}
